<!-- Licensed under a BSD license. See license.html for license -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
    <title>Three.js - Game - Just Player</title>
    <style>
    html {
      box-sizing: border-box;
    }
    *, *:before, *:after {
      box-sizing: inherit;
    }
    html, body {
        margin: 0;
        height: 100%;
    }
    #c {
        width: 100%;
        height: 100%;
        display: block;
    }
    #loading {
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      font-size: xx-large;
      font-family: sans-serif;
        }
    #loading>div>div {
      padding: 2px;
    }
    .progress {
      width: 50vw;
      border: 1px solid black;
    }
    #progressbar {
      width: 0%;
      transition: width ease-out .5s;
      height: 1em;
      background-color: #888;
      background-image: linear-gradient(
        -45deg,
        rgba(255, 255, 255, .5) 25%,
        transparent 25%,
        transparent 50%,
        rgba(255, 255, 255, .5) 50%,
        rgba(255, 255, 255, .5) 75%,
        transparent 75%,
        transparent
      );
      background-size: 50px 50px;
      animation: progressanim 2s linear infinite;
    }

    @keyframes progressanim {
      0% {
        background-position: 50px 50px;
      }
      100% {
        background-position: 0 0;
      }
    }
  </style>
  </head>
  <body>
    <canvas id="c"></canvas>
    <div id="loading">
      <div>
        <div>...loading...</div>
        <div class="progress"><div id="progressbar"></div></div>
      </div>
    </div>
  </body>
<script type="importmap">
{
  "imports": {
    "three": "../../build/three.module.js",
    "three/addons/": "../../examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

function main() {

	const canvas = document.querySelector( '#c' );
	const renderer = new THREE.WebGLRenderer( { antialias: true, canvas } );

	const fov = 45;
	const aspect = 2; // the canvas default
	const near = 0.1;
	const far = 1000;
	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
	camera.position.set( 0, 40, 80 );

	const controls = new OrbitControls( camera, canvas );
	controls.target.set( 0, 5, 0 );
	controls.update();

	const scene = new THREE.Scene();
	scene.background = new THREE.Color( 'white' );

	function addLight( ...pos ) {

		const color = 0xFFFFFF;
		const intensity = 2.5;
		const light = new THREE.DirectionalLight( color, intensity );
		light.position.set( ...pos );
		scene.add( light );
		scene.add( light.target );

	}

	addLight( 5, 5, 2 );
	addLight( - 5, 5, 5 );

	const manager = new THREE.LoadingManager();
	manager.onLoad = init;

	const progressbarElem = document.querySelector( '#progressbar' );
	manager.onProgress = ( url, itemsLoaded, itemsTotal ) => {

		progressbarElem.style.width = `${itemsLoaded / itemsTotal * 100 | 0}%`;

	};

	const models = {
		pig: { url: 'resources/models/animals/Pig.gltf' },
		cow: { url: 'resources/models/animals/Cow.gltf' },
		llama: { url: 'resources/models/animals/Llama.gltf' },
		pug: { url: 'resources/models/animals/Pug.gltf' },
		sheep: { url: 'resources/models/animals/Sheep.gltf' },
		zebra: { url: 'resources/models/animals/Zebra.gltf' },
		horse: { url: 'resources/models/animals/Horse.gltf' },
		knight: { url: 'resources/models/knight/KnightCharacter.gltf' },
	};
	{

		const gltfLoader = new GLTFLoader( manager );
		for ( const model of Object.values( models ) ) {

			gltfLoader.load( model.url, ( gltf ) => {

				model.gltf = gltf;

			} );

		}

	}

	function prepModelsAndAnimations() {

		Object.values( models ).forEach( model => {

			const animsByName = {};
			model.gltf.animations.forEach( ( clip ) => {

				animsByName[ clip.name ] = clip;
				// Should really fix this in .blend file
				if ( clip.name === 'Walk' ) {

					clip.duration /= 2;

				}

			} );
			model.animations = animsByName;

		} );

	}

	function removeArrayElement( array, element ) {

		const ndx = array.indexOf( element );
		if ( ndx >= 0 ) {

			array.splice( ndx, 1 );

		}

	}

	class SafeArray {

		constructor() {

			this.array = [];
			this.addQueue = [];
			this.removeQueue = new Set();

		}
		get isEmpty() {

			return this.addQueue.length + this.array.length > 0;

		}
		add( element ) {

			this.addQueue.push( element );

		}
		remove( element ) {

			this.removeQueue.add( element );

		}
		forEach( fn ) {

			this._addQueued();
			this._removeQueued();
			for ( const element of this.array ) {

				if ( this.removeQueue.has( element ) ) {

					continue;

				}

				fn( element );

			}

			this._removeQueued();

		}
		_addQueued() {

			if ( this.addQueue.length ) {

				this.array.splice( this.array.length, 0, ...this.addQueue );
				this.addQueue = [];

			}

		}
		_removeQueued() {

			if ( this.removeQueue.size ) {

				this.array = this.array.filter( element => ! this.removeQueue.has( element ) );
				this.removeQueue.clear();

			}

		}

	}

	class GameObjectManager {

		constructor() {

			this.gameObjects = new SafeArray();

		}
		createGameObject( parent, name ) {

			const gameObject = new GameObject( parent, name );
			this.gameObjects.add( gameObject );
			return gameObject;

		}
		removeGameObject( gameObject ) {

			this.gameObjects.remove( gameObject );

		}
		update() {

			this.gameObjects.forEach( gameObject => gameObject.update() );

		}

	}

	const globals = {
		time: 0,
		deltaTime: 0,
	};
	const gameObjectManager = new GameObjectManager();

	class GameObject {

		constructor( parent, name ) {

			this.name = name;
			this.components = [];
			this.transform = new THREE.Object3D();
			parent.add( this.transform );

		}
		addComponent( ComponentType, ...args ) {

			const component = new ComponentType( this, ...args );
			this.components.push( component );
			return component;

		}
		removeComponent( component ) {

			removeArrayElement( this.components, component );

		}
		getComponent( ComponentType ) {

			return this.components.find( c => c instanceof ComponentType );

		}
		update() {

			for ( const component of this.components ) {

				component.update();

			}

		}

	}

	// Base for all components
	class Component {

		constructor( gameObject ) {

			this.gameObject = gameObject;

		}
		update() {
		}

	}

	class SkinInstance extends Component {

		constructor( gameObject, model ) {

			super( gameObject );
			this.model = model;
			this.animRoot = SkeletonUtils.clone( this.model.gltf.scene );
			this.mixer = new THREE.AnimationMixer( this.animRoot );
			gameObject.transform.add( this.animRoot );
			this.actions = {};

		}
		setAnimation( animName ) {

			const clip = this.model.animations[ animName ];
			// turn off all current actions
			for ( const action of Object.values( this.actions ) ) {

				action.enabled = false;

			}

			// get or create existing action for clip
			const action = this.mixer.clipAction( clip );
			action.enabled = true;
			action.reset();
			action.play();
			this.actions[ animName ] = action;

		}
		update() {

			this.mixer.update( globals.deltaTime );

		}

	}

	class Player extends Component {

		constructor( gameObject ) {

			super( gameObject );
			const model = models.knight;
			this.skinInstance = gameObject.addComponent( SkinInstance, model );
			this.skinInstance.setAnimation( 'Run' );

		}

	}

	function init() {

		// hide the loading bar
		const loadingElem = document.querySelector( '#loading' );
		loadingElem.style.display = 'none';

		prepModelsAndAnimations();

		{

			const gameObject = gameObjectManager.createGameObject( scene, 'player' );
			gameObject.addComponent( Player );

		}

	}

	function resizeRendererToDisplaySize( renderer ) {

		const canvas = renderer.domElement;
		const width = canvas.clientWidth;
		const height = canvas.clientHeight;
		const needResize = canvas.width !== width || canvas.height !== height;
		if ( needResize ) {

			renderer.setSize( width, height, false );

		}

		return needResize;

	}

	let then = 0;
	function render( now ) {

		// convert to seconds
		globals.time = now * 0.001;
		// make sure delta time isn't too big.
		globals.deltaTime = Math.min( globals.time - then, 1 / 20 );
		then = globals.time;

		if ( resizeRendererToDisplaySize( renderer ) ) {

			const canvas = renderer.domElement;
			camera.aspect = canvas.clientWidth / canvas.clientHeight;
			camera.updateProjectionMatrix();

		}

		gameObjectManager.update();

		renderer.render( scene, camera );

		requestAnimationFrame( render );

	}

	requestAnimationFrame( render );

}

main();
</script>
</html>
